為什麼我們需要使用帶有 .value 的 ref,而不是普通的變量?
當你在模板中使用了一個 ref,然後改變了這個 ref 的值時,Vue 會自動檢測到這個變化,並且相應地更新 DOM。這是通過一個基於依賴追蹤的響應式系統實現的。當一個組件首次渲染時,Vue 會追蹤在渲染過程中使用的每一個 ref。然後,當一個 ref 被修改時,它會觸發追蹤它的組件的一次重新渲染。
在標準的 JavaScript 中,檢測普通變量的訪問或修改是行不通的。然而,我們可以通過 getter 和 setter 方法來攔截對象屬性的 get 和 set 操作。
該 .value 屬性給予了 Vue 一個機會來檢測 ref 何時被訪問或修改。在其內部,Vue 在它的 getter 中執行追蹤,在它的 setter 中執行觸發。從概念上講,你可以將 ref 看作是一個像這樣的對象:
// 偽代碼,並非真正的實現
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
另一個 ref 的好處是,與普通變量不同,你可以將 ref 傳遞給函數,同時保留對最新值和響應式連接的訪問。當將複雜的邏輯重構為可重用的代碼時,這將非常有用。
Ref 可以持有任何類型的值,包括深層嵌套的對象、數組或者 JavaScript 內置的數據結構,例如 Map。
Ref 會使它的值具有深層響應性。這意味著即使改變嵌套對象或數組時,變化也會被檢測到:
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都會按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
也可以通過 shallow ref 來放棄深層響應性。對於淺層 ref,只有 .value 的訪問會被追蹤。淺層 ref 可以用於避免對大型數據的響應性開銷來優化性能、或者有外部庫管理其內部狀態的情況。
當你修改了響應式狀態時,DOM 會被自動更新。但是需要注意的是,DOM 更新不是同步的。Vue 會在“next tick”更新週期中緩衝所有狀態的修改,以確保不管你進行了多少次狀態修改,每個組件都只會被更新一次。
要等待 DOM 更新完成後再執行額外的代碼,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 現在 DOM 已經更新了
}
還有另一種聲明響應式狀態的方式,即使用 reactive() API。與將內部值包裝在特殊對象中的 ref 不同,reactive() 將使對象本身具有響應性。
響應式對象是 JavaScript 代理,其行為就和普通對象一樣。不同的是,Vue 能夠攔截對響應式對象所有屬性的訪問和修改,以便進行依賴追蹤和觸發更新。
reactive() 將深層地轉換對象:當訪問嵌套對象時,它們也會被 reactive() 包裝。當 ref 的值是一個對象時,ref() 也會在內部調用它。與淺層 ref 類似,這裡也有一個 shallowReactive() API 可以選擇退出深層響應性。
Reactive Proxy vs. Original
值得注意的是,reactive() 返回的是一個原始對象的 Proxy,它和原始對象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理對象和原始對象不是全等的
console.log(proxy === raw) // false
只有代理對象是響應式的,更改原始對象不會觸發更新。因此,使用 Vue 的響應式系統的最佳實踐是僅使用你聲明對象的代理版本。
為保證訪問代理的一致性,對同一個原始對象調用 reactive() 會總是返回同樣的代理對象,而對一個已存在的代理對象調用 reactive() 會返回其本身:
// 在同一個對象上調用 reactive() 會返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一個代理上調用 reactive() 會返回它自己
console.log(reactive(proxy) === proxy) // true
這個規則對嵌套對象也適用。依靠深層響應性,響應式對象內的嵌套對象依然是代理:
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用將不再被追蹤
// (響應性連接已丟失!)
state = reactive({ count: 1 })
const state = reactive({ count: 0 })
// 當解構時,count 已經與 state.count 斷開連接
let { count } = state
// 不會影響原始的 state
count++
// 該函數接收到的是一個普通的數字
// 並且無法追蹤 state.count 的變化
// 我們必須傳入整個對象以保持響應性
callSomeFunction(state.count)
作為 reactive 對象的屬性
一個 ref 會在作為響應式對象的屬性被訪問或修改時自動解包。換句話說,它的行為就像一個普通的屬性:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果將一個新的 ref 賦值給一個關聯了已有 ref 的屬性,那麼它會替換掉舊的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 現在已經和 state.count 失去聯繫
console.log(count.value) // 1
只有當嵌套在一個深層響應式對象內時,才會發生 ref 解包。當其作為淺層響應式對象的屬性被訪問時不會解包。
與 reactive 對象不同的是,當 ref 作為響應式數組或原生集合類型 (如 Map) 中的元素被訪問時,它不會被解包:
const books = reactive([ref('Vue 3 Guide')])
// 這裡需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 這裡需要 .value
console.log(map.get('count').value)
在模板渲染上下文中,只有頂級的 ref 屬性才會被解包。
在下面的例子中,count 和 object 是頂級屬性,但 object.id 不是:
const count = ref(0)
const object = { id: ref(1) }
這可以成功解包
{{ count + 1 }}
無法解包
{{ count + 1 }}
為了解決這個問題,我們可以將 id 解構為一個頂級屬性:
const { id } = object
{{ id + 1 }}
現在渲染的結果將是 2。
另一個需要注意的點是,如果 ref 是文本插值的最終計算值 (即 {{ }} 標籤),那麼它將被解包,因此以下內容將渲染為 1:
{{ object.id }}